// Copyright 2018 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.

/// @docImport '../app.dart';
library;

import 'dart:async';

import 'package:devtools_app_shared/service.dart';
import 'package:devtools_app_shared/ui.dart';
import 'package:devtools_app_shared/utils.dart';
import 'package:devtools_shared/devtools_shared.dart';
import 'package:devtools_shared/service.dart';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import 'package:vm_service/vm_service.dart';

import '../extensions/extension_service.dart';
import '../screens/debugger/breakpoint_manager.dart';
import '../service/service_manager.dart';
import '../service/vm_service_wrapper.dart';
import '../shared/analytics/analytics.dart' as ga;
import '../shared/config_specific/framework_initialize/framework_initialize.dart';
import '../shared/console/eval/eval_service.dart';
import '../shared/framework/app_error_handling.dart' as error_handling;
import '../shared/framework/framework_controller.dart';
import '../shared/framework/screen_controllers.dart';
import '../shared/globals.dart';
import '../shared/managers/banner_messages.dart';
import '../shared/managers/notifications.dart';
import '../shared/managers/script_manager.dart';
import '../shared/managers/survey.dart';
import '../shared/offline/offline_data.dart';
import '../shared/preferences/preferences.dart';
import '../shared/primitives/message_bus.dart';
import '../shared/server/server.dart' as server;
import '../shared/utils/utils.dart';
import 'observer/memory_observer.dart';
import 'theme_manager.dart';

typedef ErrorReporter = void Function(String title, Object error);

final _log = Logger('framework_core');

/// A namespace for core framework objects and methods.
extension FrameworkCore on Never {
  static final _memoryObserver = MemoryObserver();

  /// Initializes the DevTools framework, which includes setting up global
  /// variables, local storage, preferences, and initializing framework level
  /// managers like the Dart Tooling Daemon manager and the DevTools extensions
  /// service.
  static Future<void> init() async {
    _initGlobals();

    await initializePlatform();

    // Initialize analytics metrics before initializing the rest of the
    // framework. It is important that this method call is here, after the
    // `storage` global variable has been set by the `initializePlatform` call,
    // since the analytics dimensions reference the `storage` global. We
    // initialize this now so that any analytics events sent from here on our
    // include the expected metadata.
    // TODO(kenz): consider making the dimensions that need initialization
    // `late` so that they can be initialized on first access rather than
    // manually.
    await ga.setupDimensions();

    // Print DevTools info at startup.
    _log.info(
      'Version: $devToolsVersion, Renderer: ${kIsWasm ? 'skwasm' : 'canvaskit'}',
    );

    await _initDTDConnection();

    final preferences = PreferencesController();
    // Wait for preferences to load before rendering the app to avoid a flash of
    // content with the incorrect theme.
    await preferences.init();

    // This must be called after the DTD connection has been initialized and after
    // preferences have been initialized.
    await extensionService.initialize();

    _memoryObserver.init();
  }

  /// Disposes framework level services and managers.
  ///
  /// Any service or manager that is initialized in [init] should be disposed
  /// here. This method is called from the [DevToolsAppState.dispose] lifecycle
  /// method.
  static void dispose() {
    extensionService.dispose();
    preferences.dispose();
    _themeManager?.dispose();
    unawaited(dtdManager.dispose());
    _memoryObserver.dispose();
  }

  static void _initGlobals() {
    setGlobal(ScreenControllers, ScreenControllers());
    setGlobal(ServiceConnectionManager, ServiceConnectionManager());
    setGlobal(MessageBus, MessageBus());
    setGlobal(FrameworkController, FrameworkController());
    setGlobal(SurveyService, SurveyService());
    setGlobal(OfflineDataController, OfflineDataController());
    setGlobal(ScriptManager, ScriptManager());
    setGlobal(NotificationService, NotificationService());
    setGlobal(BannerMessagesController, BannerMessagesController());
    setGlobal(BreakpointManager, BreakpointManager());
    setGlobal(EvalService, EvalService());
    setGlobal(ExtensionService, ExtensionService());
    setGlobal(IdeTheme, getIdeTheme());
    setGlobal(DTDManager, DTDManager());
  }

  static bool vmServiceInitializationInProgress = false;

  static EditorThemeManager? _themeManager;

  /// Attempts to initialize a VM service connection and return whether the
  /// connection attempt succeeded.
  static Future<bool> initVmService({
    required String serviceUriAsString,
    ErrorReporter? errorReporter = _defaultErrorReporter,
    bool logException = true,
  }) async {
    if (serviceConnection.serviceManager.connectedState.value.connected) {
      // TODO(https://github.com/flutter/devtools/issues/1568): why do we call
      // this multiple times?
      return true;
    }

    final uri = normalizeVmServiceUri(serviceUriAsString);
    if (uri != null) {
      vmServiceInitializationInProgress = true;
      final finishedCompleter = Completer<void>();

      try {
        final service = await connect<VmServiceWrapper>(
          uri: uri,
          finishedCompleter: finishedCompleter,
          serviceFactory:
              ({
                // ignore: avoid-dynamic, mirrors types of [VmServiceFactory].
                required Stream<dynamic> /*String|List<int>*/ inStream,
                required void Function(String message) writeMessage,
                Log? log,
                DisposeHandler? disposeHandler,
                Future? streamClosed,
                String? wsUri,
                bool trackFutures = false,
              }) => VmServiceWrapper.defaultFactory(
                inStream: inStream,
                writeMessage: writeMessage,
                log: log,
                disposeHandler: disposeHandler,
                streamClosed: streamClosed,
                wsUri: wsUri,
                trackFutures: integrationTestMode,
              ),
        );

        await serviceConnection.serviceManager.vmServiceOpened(
          service,
          onClosed: finishedCompleter.future,
        );
        await breakpointManager.initialize();
        return true;
      } catch (e, st) {
        if (logException) {
          _log.shout(e, e, st);
        }
        errorReporter!('Unable to connect to VM service at $uri: $e', e);
        return false;
      } finally {
        vmServiceInitializationInProgress = false;
      }
    } else {
      // Don't report an error here because we do not have a URI to connect to.
      return false;
    }
  }

  static void _defaultErrorReporter(String title, Object error) {
    notificationService.pushError('$title, $error', isReportable: false);
  }
}

Future<void> _initDTDConnection() async {
  try {
    // Get the dtdUri from the devtools server
    final dtdUri = await server.getDtdUri();

    if (dtdUri != null) {
      await dtdManager.connect(
        dtdUri,
        onError: (e, st) {
          notificationService.pushError(
            'Failed to connect to the Dart Tooling Daemon',
            isReportable: false,
          );
          error_handling.reportError(
            e,
            errorType: 'Dart Tooling Daemon connection failed.',
            stack: st,
          );
        },
      );

      if (dtdManager.connection.value != null) {
        FrameworkCore._themeManager = EditorThemeManager(
          dtdManager.connection.value!,
        )..listenForThemeChanges();
      }
    } else {
      _log.info('No DTD uri provided from the server during initialization.');
    }
  } catch (e, st) {
    // Dtd failing to connect does not interfere with devtools starting up so
    // catch any errors and report them.
    error_handling.reportError(
      e,
      errorType: 'Failed to initialize the DTD connection.',
      stack: st,
    );
  }
}
